1 Purpose and Research Questions

This analysis examines whether changes in federal spending between FY2020 and FY2024:

  1. Varied meaningfully across states
  2. Were associated with Biden’s 2020 electoral support
  3. Were associated with 2024 election outcomes for President, Senate, and House

The analysis is observational and evaluates correlation, not causation.

2 Libraries

library(tidyverse)
library(janitor)
library(lubridate)
library(httr)
library(jsonlite)
library(readxl)
library(scales)
library(ggrepel)
library(usmap)

3 Minimal-Ink Visualization Theme

4 Low-Ink Visualization Style Guide (bbplot)

This document uses a single low-ink-to-data style guide for consistent, uncluttered charts.

library(ggplot2)
library(scales)

if (!requireNamespace("bbplot", quietly = TRUE)) {
  if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes")
  remotes::install_github("bbc/bbplot")
}
library(bbplot)

# Strict low-ink theme: no gridlines, no axis lines, small labels
theme_low_ink <- function(base_size = 11) {
  bbplot::bbc_style() %+replace% theme(
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank(),
    panel.grid.major.y = element_blank(),
    panel.grid.minor.y = element_blank(),
    axis.line = element_blank(),
    axis.ticks = element_blank(),
    axis.text.y = element_text(size = base_size * 0.6),
    axis.text.x = element_text(size = base_size * 0.7),
    legend.title = element_blank(),
    legend.position = "top",
    plot.title.position = "plot",
    plot.title = element_text(margin = margin(b = 6)),
    plot.subtitle = element_text(margin = margin(b = 8))
  )
}

theme_set(theme_low_ink())

# Party color scale (muted, low-ink)
scale_party_fill <- function(...) {
  scale_fill_manual(
    values = c(
      "Biden" = "#2166AC",  # muted Dem blue
      "Trump" = "#B2182B"   # muted GOP red
    ),
    ...
  )
}

label_billions <- label_dollar(scale = 1e-9, suffix = "B", accuracy = 0.1)
label_dollars1 <- label_dollar(accuracy = 1)
label_pct1 <- label_percent(accuracy = 1)

5 Federal Spending Data (USAspending)

usaspending_post <- function(endpoint, body) {
  res <- POST(
    url = paste0("https://api.usaspending.gov", endpoint),
    body = body,
    encode = "json",
    add_headers(`Content-Type` = "application/json")
  )
  stop_for_status(res)
  content(res, as = "parsed", simplifyVector = TRUE)
}

get_state_obligations <- function(fy) {
  body <- list(
    scope = "place_of_performance",
    geo_layer = "state",
    filters = list(
      time_period = list(list(
        start_date = paste0(fy - 1, "-10-01"),
        end_date   = paste0(fy, "-09-30")
      ))
    )
  )

  out <- usaspending_post("/api/v2/search/spending_by_geography/", body)

  # Robustly normalize the results payload. Depending on httr/content parsing,
  # `out$results` may be a data.frame/tibble OR a list of records.
  res <- out$results
  if (is.null(res) || length(res) == 0) {
    return(tibble(state = character(), fiscal_year = integer(), obligations = numeric()))
  }

  if (is.data.frame(res)) {
    return(tibble(
      state = toupper(res$shape_code),
      fiscal_year = fy,
      obligations = res$aggregated_amount
    ))
  }

  tibble(
    state = toupper(purrr::map_chr(res, "shape_code")),
    fiscal_year = fy,
    obligations = purrr::map_dbl(res, "aggregated_amount")
  )
}
library(tidyverse)
library(janitor)
library(scales)

# Compare baseline vs FY2024 using alpha (no patterns)

# Ensure population table exists (created in the population chunk). If knitting from the middle,
# we'll load it from disk to avoid execution-order issues.
if (!exists("pop")) {
  if (file.exists("population_by_state_fy.csv")) {
    pop <- readr::read_csv("population_by_state_fy.csv", show_col_types = FALSE) |>
      janitor::clean_names() |>
      dplyr::transmute(
        state = toupper(state),
        fiscal_year = as.integer(fiscal_year),
        pop = as.numeric(pop)
      )
    message("Loaded pop from population_by_state_fy.csv")

# Ensure pres2020 exists (created in pres2020 chunk). If knitting from the middle,
# we'll derive it from local election files.
if (!exists("pres2020")) {
  if (file.exists("president_2020.csv")) {

    pres_long <- readr::read_csv("president_2020.csv", show_col_types = FALSE) |>
      janitor::clean_names() |>
      dplyr::filter(year == 2020) |>
      dplyr::mutate(state = toupper(state_po))

  } else if (file.exists("1976-2020-president.csv")) {

    pres_long <- readr::read_csv("1976-2020-president.csv", show_col_types = FALSE) |>
      janitor::clean_names() |>
      dplyr::filter(year == 2020) |>
      dplyr::mutate(state = toupper(state_po))

  } else {
    stop("pres2020 not found and no election file present. Add president_2020.csv (preferred) or 1976-2020-president.csv, or knit from the top.")
  }

  pres2020 <- pres_long |>
    dplyr::group_by(state) |>
    dplyr::summarize(
      votes_biden = sum(candidatevotes[stringr::str_detect(toupper(candidate), "BIDEN")], na.rm = TRUE),
      votes_trump = sum(candidatevotes[stringr::str_detect(toupper(candidate), "TRUMP")], na.rm = TRUE),
      biden_share_2p = votes_biden / (votes_biden + votes_trump),
      biden_won = votes_biden > votes_trump,
      .groups = "drop"
    ) |>
    dplyr::mutate(
      winner_2020 = dplyr::if_else(biden_won, "Biden", "Trump"),
      winner_2020 = factor(winner_2020, levels = c("Biden", "Trump"))
    )

  message("Derived pres2020 from local election file.")
}

  } else {
    stop("Population table `pop` not found. Knit from the top (Run All), or ensure the population chunk runs first to create population_by_state_fy.csv.")
  }
}


# Pull spending for FY2017–FY2024 (needed for Pre vs Biden baseline and FY2024 levels)
spending <- purrr::map_dfr(2017:2024, get_state_obligations)

# Sanity check to avoid blank charts
spend_check <- spending |>
  count(fiscal_year, name = "n_states") |>
  arrange(fiscal_year)
print(spend_check)
## # A tibble: 8 × 2
##   fiscal_year n_states
##         <int>    <int>
## 1        2017       57
## 2        2018       57
## 3        2019       57
## 4        2020       57
## 5        2021       57
## 6        2022       57
## 7        2023       57
## 8        2024       57
if (nrow(spending) == 0) stop("USAspending returned 0 rows. Check connectivity/API response.")
if (any(spend_check$n_states < 40)) warning("Some years returned fewer than 40 states; plots may be incomplete.")

# Merge population for per-capita measures (pop is created in the population chunk)
if (!exists("pop")) stop("Population table `pop` not found. Ensure the population chunk runs before this chunk.")

spending_pc <- spending |>
  left_join(pop, by = c("state","fiscal_year")) |>
  mutate(oblig_pc = obligations / pop)

# Pre-Biden baseline (FY17–FY20 avg) and Biden era (FY21–FY24 avg)
spend_era <- spending_pc |>
  mutate(
    era = case_when(
      fiscal_year %in% 2017:2020 ~ "Pre (FY17–FY20 avg)",
      fiscal_year %in% 2021:2024 ~ "Biden (FY21–FY24 avg)",
      TRUE ~ NA_character_
    )
  ) |>
  filter(!is.na(era)) |>
  group_by(state, era) |>
  summarize(
    total = mean(obligations, na.rm = TRUE),
    pc    = mean(oblig_pc, na.rm = TRUE),
    .groups = "drop"
  ) |>
  pivot_wider(names_from = era, values_from = c(total, pc)) |>
  rename(
    pre_total   = `total_Pre (FY17–FY20 avg)`,
    biden_total = `total_Biden (FY21–FY24 avg)`,
    pre_pc      = `pc_Pre (FY17–FY20 avg)`,
    biden_pc    = `pc_Biden (FY21–FY24 avg)`
  ) |>
  mutate(
    delta_biden_vs_pre_total = biden_total - pre_total,
    delta_biden_vs_pre_pc    = biden_pc - pre_pc
  )

# FY2024 levels (solid bars)
fy2024 <- spending_pc |>
  filter(fiscal_year == 2024) |>
  select(state, total_2024 = obligations, pc_2024 = oblig_pc)

# Bar chart data: baseline (striped) vs FY2024 (solid), colored by 2020 winner
if (!exists("pres2020")) stop("pres2020 not found. Ensure the pres2020 chunk runs before the bar charts.")

bars_total <- fy2024 |>
  select(state, value = total_2024) |>
  mutate(series = "FY2024") |>
  bind_rows(
    spend_era |>
      select(state, value = pre_total) |>
      mutate(series = "Pre (FY17–FY20 avg)")
  ) |>
  left_join(pres2020 |> select(state, winner_2020), by = "state") |>
  mutate(series = factor(series, levels = c("Pre (FY17–FY20 avg)", "FY2024")))

bars_pc <- fy2024 |>
  select(state, value = pc_2024) |>
  mutate(series = "FY2024") |>
  bind_rows(
    spend_era |>
      select(state, value = pre_pc) |>
      mutate(series = "Pre (FY17–FY20 avg)")
  ) |>
  left_join(pres2020 |> select(state, winner_2020), by = "state") |>
  mutate(series = factor(series, levels = c("Pre (FY17–FY20 avg)", "FY2024")))

# Section 12.1 delta dataset
delta_12_1 <- spend_era |>
  left_join(pres2020 |> select(state, winner_2020), by = "state")

6 Population Data (User-Supplied)

Provide a CSV named population_by_state_fy.csv with columns:

library(tidyverse)
library(janitor)
library(readr)
library(stringr)

# Build accurate state populations for FY2017–FY2024 using Census Population Estimates:
# - 2010–2019 series for 2017–2019
# - 2020–2024 series for 2020–2024
#
# If the source CSVs are not present, this chunk will download them from Census.

POP_OUT <- "population_by_state_fy.csv"

CENSUS_2010S_FILE <- "NST-EST2019-ALLDATA.csv"
CENSUS_2020S_FILE <- "NST-EST2024-ALLDATA.csv"

CENSUS_2010S_URL <- "https://www2.census.gov/programs-surveys/popest/datasets/2010-2019/national/totals/nst-est2019-alldata.csv"
CENSUS_2020S_URL <- "https://www2.census.gov/programs-surveys/popest/datasets/2020-2024/state/totals/NST-EST2024-ALLDATA.csv"

# If you've already created POP_OUT, we use it for speed + reproducibility.
if (file.exists(POP_OUT)) {

  pop <- read_csv(POP_OUT, show_col_types = FALSE) |>
    clean_names() |>
    transmute(
      state = toupper(state),
      fiscal_year = as.integer(fiscal_year),
      pop = as.numeric(pop)
    )

} else {

  # Download Census source files if missing
  if (!file.exists(CENSUS_2010S_FILE)) {
    download.file(CENSUS_2010S_URL, destfile = CENSUS_2010S_FILE, mode = "wb", quiet = TRUE)
  }
  if (!file.exists(CENSUS_2020S_FILE)) {
    download.file(CENSUS_2020S_URL, destfile = CENSUS_2020S_FILE, mode = "wb", quiet = TRUE)
  }

  pop_2010s_raw <- read_csv(CENSUS_2010S_FILE, show_col_types = FALSE) |> clean_names()
  pop_2020s_raw <- read_csv(CENSUS_2020S_FILE, show_col_types = FALSE) |> clean_names()

  # Keep only state-level rows: SUMLEV == 40 (states), plus DC (also SUMLEV 40 in these files)
  # Puerto Rico is included; keep it if you want, but our later charts focus on states + DC.
  pop_2010s <- pop_2010s_raw |>
    filter(sumlev == 40) |>
    select(name, starts_with("popestimate")) |>
    mutate(name = toupper(name))

  pop_2020s <- pop_2020s_raw |>
    filter(sumlev == 40) |>
    select(name, starts_with("popestimate")) |>
    mutate(name = toupper(name))

  # State name -> abbreviation (50 states + DC)
  state_lu <- tibble(
    name = toupper(state.name),
    state = state.abb
  ) |>
    add_row(name = "DISTRICT OF COLUMBIA", state = "DC")

  # 2017–2019 from 2010s file; 2020–2024 from 2020s file
  pop_2017_2019 <- pop_2010s |>
    inner_join(state_lu, by = "name") |>
    pivot_longer(
      cols = matches("^popestimate(2017|2018|2019)$"),
      names_to = "year",
      values_to = "pop"
    ) |>
    mutate(
      fiscal_year = as.integer(str_extract(year, "\\d{4}")),
      pop = as.numeric(pop)
    ) |>
    select(state, fiscal_year, pop)

  pop_2020_2024 <- pop_2020s |>
    inner_join(state_lu, by = "name") |>
    pivot_longer(
      cols = matches("^popestimate(2020|2021|2022|2023|2024)$"),
      names_to = "year",
      values_to = "pop"
    ) |>
    mutate(
      fiscal_year = as.integer(str_extract(year, "\\d{4}")),
      pop = as.numeric(pop)
    ) |>
    select(state, fiscal_year, pop)

  pop <- bind_rows(pop_2017_2019, pop_2020_2024) |>
    arrange(state, fiscal_year)

  # Save a tidy, analysis-ready file for your zip bundle
  write_csv(pop, POP_OUT)
  message("Created ", POP_OUT, " using Census sources: ", CENSUS_2010S_FILE, " and ", CENSUS_2020S_FILE)
}

# Quick check: should cover FY2017–FY2024 for 51 entities (50 states + DC)
pop_check <- pop |>
  filter(fiscal_year %in% 2017:2024) |>
  count(fiscal_year, name = "n_states") |>
  arrange(fiscal_year)

print(pop_check)
## # A tibble: 5 × 2
##   fiscal_year n_states
##         <int>    <int>
## 1        2020       51
## 2        2021       51
## 3        2022       51
## 4        2023       51
## 5        2024       51

7 Spending Deltas FY2020–FY2024

# OPTION 1: Per-capita delta defined as FY2024 − FY2020 (states + DC only)

valid_states <- c(state.abb, "DC")

# For election-correlation charts, restrict to states + DC
spending_pc_states <- spending_pc |>
  mutate(state = toupper(state)) |>
  filter(state %in% valid_states)

spending_delta <- spending_pc_states |>
  filter(fiscal_year %in% c(2020, 2024)) |>
  select(state, fiscal_year, obligations, oblig_pc) |>
  pivot_wider(
    names_from = fiscal_year,
    values_from = c(obligations, oblig_pc),
    names_prefix = "fy"
  ) |>
  mutate(
    delta_pc = oblig_pc_fy2024 - oblig_pc_fy2020,
    delta_total = obligations_fy2024 - obligations_fy2020
  )

# Sanity check: should be ~51 rows and delta_pc should be non-missing
print(spending_delta |>
        summarize(n_rows = n(), n_delta_pc = sum(!is.na(delta_pc)), n_delta_total = sum(!is.na(delta_total))))
## # A tibble: 1 × 3
##   n_rows n_delta_pc n_delta_total
##    <int>      <int>         <int>
## 1     51         51            51

8 2020 Presidential Winner by State (for color encoding)

Bars are colored by whether the state’s electoral votes went to Biden or Trump in 2020.
Provide a CSV named president_2020.csv in this folder (MIT Election Lab export recommended) with at least:

PRES2020_FILE <- "president_2020.csv"

if (!file.exists(PRES2020_FILE)) {
  stop("Missing file: president_2020.csv. Put it in the same folder as this Rmd to color bars by Biden/Trump in 2020.")
}

pres_long <- read_csv(PRES2020_FILE, show_col_types = FALSE) |>
  clean_names() |>
  filter(year == 2020) |>
  mutate(state = toupper(state_po))

pres2020 <- pres_long |>
  group_by(state) |>
  summarize(
    votes_biden = sum(candidatevotes[str_detect(toupper(candidate), "BIDEN")], na.rm = TRUE),
    votes_trump = sum(candidatevotes[str_detect(toupper(candidate), "TRUMP")], na.rm = TRUE),
    biden_share_2p = votes_biden / (votes_biden + votes_trump),
    biden_won = votes_biden > votes_trump,
    .groups = "drop"
  ) |>
  mutate(
    winner_2020 = if_else(biden_won, "Biden", "Trump"),
    winner_2020 = factor(winner_2020, levels = c("Biden", "Trump"))
  )

9 Bar Charts (FY2024): Spending and Spending Per Capita

These charts use a horizontal bar layout for readable state labels, and follow the low-ink theme set above.

9.1 FY2024 Obligations by State (Total)

# Ensure consistent series ordering within each state: Pre above FY2024
pre_lab <- "Pre (FY17–FY20 avg)"
fy24_lab <- "FY2024"

# Order states by the maximum of the two series for readability
state_order_total <- bars_total |>
  group_by(state) |>
  summarize(max_val = max(value, na.rm = TRUE), .groups = "drop") |>
  arrange(max_val) |>
  pull(state)

# Force y-axis levels: for each state, Pre then FY2024
y_levels_total <- c(rbind(
  paste0(state_order_total, "  ", pre_lab),
  paste0(state_order_total, "  ", fy24_lab)
))

bars_total_plot <- bars_total |>
  mutate(
    series = factor(series, levels = c(pre_lab, fy24_lab)),
    alpha_series = if_else(series == pre_lab, 0.5, 1.0),
    state_series = paste0(state, "  ", as.character(series)),
    state_series = factor(state_series, levels = y_levels_total)
  )

ggplot(bars_total_plot, aes(x = value, y = state_series, fill = winner_2020, alpha = alpha_series)) +
  geom_col(width = 0.75) +
  scale_party_fill() +
  scale_alpha_identity(guide = "none") +
  scale_y_discrete(labels = function(x) {
    st <- sub("  .*", "", x)
    ifelse(duplicated(st), "", st)
  }) +
  scale_x_continuous(labels = label_billions) +
  labs(
    title = "Federal obligations by state: FY2024 vs pre-Biden baseline",
    subtitle = "Top bar per state = mean(FY2017–FY2020); bottom bar = FY2024",
    x = "Obligations (billions of $)",
    y = NULL
  ) +
  theme_low_ink() +
  theme(
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank(),
    axis.line = element_blank(),
    axis.ticks = element_blank(),
    axis.text.y = element_text(size = 7)
  )

9.2 FY2024 Obligations Per Capita by State

# Ensure consistent series ordering within each state: Pre above FY2024
pre_lab <- "Pre (FY17–FY20 avg)"
fy24_lab <- "FY2024"

# Order states by the maximum of the two series for readability
state_order_pc <- bars_pc |>
  group_by(state) |>
  summarize(max_val = max(value, na.rm = TRUE), .groups = "drop") |>
  arrange(max_val) |>
  pull(state)

# Force y-axis levels: for each state, Pre then FY2024
y_levels_pc <- c(rbind(
  paste0(state_order_pc, "  ", pre_lab),
  paste0(state_order_pc, "  ", fy24_lab)
))

bars_pc_plot <- bars_pc |>
  mutate(
    series = factor(series, levels = c(pre_lab, fy24_lab)),
    alpha_series = if_else(series == pre_lab, 0.5, 1.0),
    state_series = paste0(state, "  ", as.character(series)),
    state_series = factor(state_series, levels = y_levels_pc)
  )

ggplot(bars_pc_plot, aes(x = value, y = state_series, fill = winner_2020, alpha = alpha_series)) +
  geom_col(width = 0.75) +
  scale_party_fill() +
  scale_alpha_identity(guide = "none") +
  scale_y_discrete(labels = function(x) {
    st <- sub("  .*", "", x)
    ifelse(duplicated(st), "", st)
  }) +
  scale_x_continuous(labels = label_dollars1) +
  labs(
    title = "Federal obligations per capita by state: FY2024 vs pre-Biden baseline",
    subtitle = "Top bar per state = mean(FY2017–FY2020); bottom bar = FY2024",
    x = "Obligations per capita ($)",
    y = NULL
  ) +
  theme_low_ink() +
  theme(
    panel.grid.major = element_blank(),
    panel.grid.minor = element_blank(),
    axis.line = element_blank(),
    axis.ticks = element_blank(),
    axis.text.y = element_text(size = 7)
  )

10 2024 Election Results

states_2024 <- read_csv("https://michaelminn.net/tutorials/data/2024-electoral-states.csv") |> clean_names()
districts_2024 <- read_csv("https://michaelminn.net/tutorials/data/2024-electoral-districts.csv") |> clean_names()

pres_2024 <- states_2024 |>
  transmute(
    state = st,
    pres_margin_dem = (votes_dem_2024 - votes_gop_2024) /
      (votes_dem_2024 + votes_gop_2024)
  )

house_2024 <- districts_2024 |>
  group_by(st) |>
  summarize(
    house_margin_dem =
      (sum(votes_dem_2024) - sum(votes_gop_2024)) /
      (sum(votes_dem_2024) + sum(votes_gop_2024)),
    .groups = "drop"
  ) |>
  rename(state = st)

elections_2024 <- left_join(pres_2024, house_2024, by = "state")

11 Merge Spending and Elections

analysis <- spending_delta |>
  left_join(elections_2024, by = "state")

12 Results

12.1 Spending Change vs 2024 Presidential Margin

ggplot(analysis, aes(delta_pc, pres_margin_dem)) +
  geom_hline(yintercept = 0, linewidth = 0.25) +
  geom_vline(xintercept = 0, linewidth = 0.25) +
  geom_point() +
  geom_smooth(method = "lm", se = FALSE) +
  scale_x_continuous(labels = dollar) +
  scale_y_continuous(labels = percent) +
  labs(
    title = "Change in Federal Spending vs 2024 Presidential Margin",
    x = "Δ Federal Obligations per Capita (FY2024 − FY2020)",
    y = "2024 Presidential Margin (Dem − GOP)"
  )

12.2 Key Observations

What This Chart Shows:

This scatter plot examines whether changes in federal spending per capita between FY2020 and FY2024 correlate with how states voted in the 2024 presidential election. Each point represents one state (or DC), with:

  • X-axis: Change in federal obligations per capita (positive = more spending in 2024; negative = less)
  • Y-axis: 2024 Presidential margin (positive = Democratic win; negative = Republican win)
  • Fitted line: Linear regression showing the overall trend across all jurisdictions

12.2.1 Analytical Findings:

  1. Weak or Non-Existent Linear Relationship: The fitted line’s slope indicates little systematic correlation between changes in federal spending and how states voted in the 2024 presidential election.

  2. Wide Scatter Distribution: States are distributed across all four quadrants, demonstrating that:

  • Some states received more spending but still voted Republican
  • Some states received less spending but voted Democratic
  • The relationship is not deterministic—many other factors influence voting behavior
  1. No Evidence of Electoral Spending Strategy: The weak correlation suggests federal spending changes did not translate into predictable electoral support in 2024.

  2. Outlier Influence: Individual states that deviate significantly from the trend line represent cases where local political, economic, or demographic factors overwhelmed any spending-related patterns.

12.3 Spending Change vs 2024 House Margin

ggplot(analysis, aes(delta_pc, house_margin_dem)) +
  geom_hline(yintercept = 0, linewidth = 0.25) +
  geom_vline(xintercept = 0, linewidth = 0.25) +
  geom_point() +
  geom_smooth(method = "lm", se = FALSE) +
  scale_x_continuous(labels = dollar) +
  scale_y_continuous(labels = percent) +
  labs(
    title = "Change in Federal Spending vs 2024 House Margin",
    x = "Δ Federal Obligations per Capita (FY2024 − FY2020)",
    y = "2024 House Margin (Dem − GOP)"
  )

12.4 Key Observations

What This Chart Shows:

This scatter plot tests whether federal spending changes correlate with aggregate House election outcomes at the state level. The analysis uses:

  • X-axis: Change in federal obligations per capita (FY2024 − FY2020)
  • Y-axis: State-level House margin (total Democratic votes − total Republican votes across all districts)
  • Fitted line: Linear regression trend

12.4.1 Analytical Findings:

  1. Similar Pattern to Presidential Results: House margins show a comparable relationship to spending changes as presidential results, suggesting federal spending patterns didn’t strongly influence congressional voting either.

  2. More Granular Electoral Data: House margins aggregate many districts within each state, providing a different measure of political sentiment that may be less sensitive to statewide spending changes.

  3. State-Level Aggregation Limitations: Since House races are district-by-district, aggregating to state-level obscures local variations—some districts may have benefited greatly from spending while others didn’t.

  4. Consistency Across Election Types: The similarity between presidential and House scatter patterns reinforces that the spending-voting relationship (or lack thereof) is consistent across different types of federal elections.

  5. Observational Correlation Only: These findings represent correlation, not causation. Federal spending decisions, electoral outcomes, and numerous confounding variables interact in complex ways that this analysis cannot disentangle.

13 Section 12.1 — Biden-era Delta vs Pre-Biden Baseline

delta_plot <- delta_12_1 |>
  mutate(delta = delta_biden_vs_pre_pc) |>
  arrange(delta) |>
  mutate(state = factor(state, levels = state))

ggplot(delta_plot, aes(x = delta, y = state, fill = winner_2020)) +
  geom_col(width = 0.75) +
  scale_party_fill(guide = "none") +
  scale_x_continuous(labels = label_dollars1) +
  labs(
    title = "Change in obligations per capita: Biden era vs pre-Biden baseline",
    subtitle = "Δ = mean(FY2021–FY2024) − mean(FY2017–FY2020)",
    x = "Δ obligations per capita ($)",
    y = NULL
  ) +
  theme_low_ink()

13.1 Key Observations

What This Chart Shows:

This bar chart compares average federal obligations per capita during the Biden era (FY2021–FY2024) against the pre-Biden baseline (FY2017–FY2020). Bars extending right indicate increased spending; bars extending left indicate decreased spending. Colors represent which candidate won each state in 2020 (blue = Biden, red = Trump).

13.1.1 Spending Growth Patterns:

  1. North Dakota Shows Dramatic Decline: ND experienced the largest negative change (approximately −$10,000 per capita), representing a massive reduction in federal obligations. Potential explanations:
  • Reduced energy sector subsidies reflecting Biden administration’s shift away from fossil fuel support
  • Completion of major infrastructure projects active during FY2017–2020
  • Changes in agricultural support programs
  • Wind-down of pandemic-related emergency spending that disproportionately flowed to ND in FY2020
  1. DC Shows Strong Positive Growth: Washington DC experienced significant increases in per-capita federal obligations during the Biden era, likely reflecting:
  • Increased federal workforce spending and operations
  • Infrastructure investments in the nation’s capital
  • Expansion of federal programs headquartered in DC
  • Biden administration’s emphasis on strengthening federal institutions
  1. Small Population States Show Extremes: States like ND (large negative) and DC (positive) show the most dramatic per-capita swings because small denominators amplify the impact of absolute dollar changes.

13.1.2 Political Alignment Patterns:

  1. Mixed Partisan Pattern: Both Trump-won states (red) and Biden-won states (blue) appear across the entire spectrum from large negative to large positive changes, though some notable patterns emerge

  2. ND and DC as Extreme Cases:

  • ND (solidly Republican) experienced the largest decline under a Democratic administration
  • DC (overwhelmingly Democratic) gained significantly
  • These cases could support claims of partisan allocation, though sectoral policy explanations (energy sector decline in ND, federal operations expansion in DC) are equally or more plausible
  1. Trump States in Both Directions: Republican-won states appear in both positive and negative change categories. If partisan favoritism were systematic, we would expect most or all Republican states clustered in the negative range—but this is not the case.

  2. Biden States Also Mixed: Democratic-won states similarly show both increases and decreases, suggesting that 2020 electoral outcomes alone do not predict spending changes.

13.1.3 Sectoral and Policy Implications:

  1. Energy State Decline: ND’s massive decline likely reflects sectoral policy shifts—the Biden administration’s movement away from fossil fuel development would disproportionately impact major oil and gas producing states.

  2. Policy Over Politics: The distribution pattern suggests federal spending changes were driven primarily by:

  • Sectoral policy priorities (energy transition, infrastructure modernization)
  • Economic conditions and programmatic needs
  • Administrative policy shifts (pandemic program wind-downs, new infrastructure initiatives)
  • Rather than electoral reward/punishment strategies
  1. Program Composition Matters: Understanding which specific programs changed (defense spending, Medicare/Medicaid, infrastructure grants, energy subsidies, agricultural support) would be essential to fully explain these patterns.

13.1.4 Analytical Significance:

  1. Challenges Simple Narratives: This chart provides evidence against claims that the Biden administration systematically “rewarded” states that voted Democratic in 2020—the data show no clear partisan pattern.

  2. Complex Causality: The wide variation across states with different political alignments suggests federal spending patterns result from complex interactions of policy priorities, economic needs, programmatic structures, and institutional factors—not simple political calculations.

  3. Observational Analysis: These findings represent observed correlations between spending changes and political characteristics. Causation cannot be inferred—we cannot determine whether political factors influenced spending, whether spending influenced politics, or whether both were driven by other factors.

14 Exporting Charts to PowerPoint

You have two recommended options:

14.1 Option A — PNG workflow (simple)

  1. Knit this document.
  2. Right-click figures in the HTML output and save as images.
  3. Insert images into PowerPoint using Insert → Pictures.

15 Notes